/*
 * Copyright (C) 2010 Fores Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jp.fores.midori.server.plugin;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

import jp.fores.midori.server.exception.IllegalServiceClassException;
import jp.fores.midori.server.exception.RPCTargetMethodNotFoundException;


/**
 * RPCの対象のメソッド情報を管理するクラス。<br>
 * <i>(SingletonパターンのSingleton役)</i><br>
 * (このクラスの全てのメソッドはスレッドセーフです。)<br>
 */
public class RPCTargetMethodManager {
	//==========================================================
	//定数

	/**
	 * 自分自身のクラスの唯一のインスタンス
	 */
	private static final RPCTargetMethodManager singleton = new RPCTargetMethodManager();


	//==========================================================
	//フィールド

	/**
	 * メソッド情報をキャッシュするMap
	 */
	//(最初のキーがクラス名、2番目のキーがメソッド名と引数の数の入れ子構造のMap)
	private final Map<String, Map<MethodNameAndArgsLength, Method>> methodCache = new HashMap<String, Map<MethodNameAndArgsLength, Method>>();


	//==========================================================
	//メソッド

	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//コンストラクタ

	/**
	 * コンストラクタです。<br>
	 * (Singletonクラスなので、privateにして外部からインスタンスを作成できないようにしています。)<br>
	 */
	private RPCTargetMethodManager() {
		//特に何も行わない
	}


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//クラスメソッド

	/**
	 * このクラスの唯一のインスタンスを返します。
	 *
	 * @return このクラスの唯一のインスタンス
	 */
	public static RPCTargetMethodManager getInstance() {
		//フィールドの自分自身のクラスの唯一のインスタンスを返します
		return singleton;
	}


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//通常のメソッド

	/**
	 * 指定されたオブジェクトの、メソッド名と引数の配列に適合するRPC対象のメソッドを取得します。<br>
	 * 指定されたオブジェクトがRPCの対象クラスとしてふさわしくない場合や、適合するメソッドが存在しない場合は例外を投げます。<br>
	 *
	 * @param obj オブジェクト
	 * @param methodName メソッド名
	 * @param args 引数の配列
	 * @return 適合するメソッド
	 * @throws IllegalServiceClassException 指定されたオブジェクトがRPCの対象クラスとしてふさわしくない場合
	 * @throws RPCTargetMethodNotFoundException 適合するメソッドが存在しない場合
	 * @throws Exception 例外
	 */
	public Method findRPCTargetMethod(Object obj, String methodName,
			Object[] args) throws IllegalServiceClassException,
			RPCTargetMethodNotFoundException, Exception {
		//==========================================================
		//引数のオブジェクトに対応するRPCの対象となるメソッド情報のMapを取得する

		//引数のオブジェクトのクラスオブジェクトを取得
		Class clazz = obj.getClass();

		//クラスオブジェクトからクラス名を取得
		String className = clazz.getName();

		//RPCの対象となるメソッド情報のMapを取得する
		Map<MethodNameAndArgsLength, Method> rpcTargetMethodMap = getRPCTargetMethodMap(clazz);


		//==========================================================
		//RPCの対象となるメソッド情報のMapから、メソッド名と引数の数が一致する要素を取得する

		//引数の数
		int argsLength = 0;

		//引数の配列がnullでない場合
		if (args != null) {
			//引数の配列の要素数を取得して、引数の数に設定
			argsLength = args.length;
		}

		//メソッド名と引数の数を保持するクラスのインスタンスを生成
		MethodNameAndArgsLength methodNameAndArgsLength = new MethodNameAndArgsLength(methodName, argsLength);

		//メソッド名と引数の数を保持するクラスのインスタンスをキーにして、RPCの対象となるメソッド情報のMapからメソッドを取得する
		Method method = rpcTargetMethodMap.get(methodNameAndArgsLength);

		//メソッドが取得できなかった場合
		if (method == null) {
			//RPCの対象のメソッドが見つからなかった場合の例外を投げる
			throw new RPCTargetMethodNotFoundException(className + "クラスには"
					+ methodNameAndArgsLength.methodName + "()メソッドで引数の数が"
					+ methodNameAndArgsLength.argsLength + "のものが定義されていません。");
		}

		//取得したメソッドを返す
		return method;
	}

	/**
	 * RPCの対象となるメソッドかどうか判断します。<br>
	 * (本来は内部処理用のメソッドですが、外部からも呼び出させるようにpublicで定義しています。)<br>
	 *
	 * @param method チェック対象のメソッド
	 * @return true=RPCの対象となるメソッドの場合, false=RPCの対象となるメソッドでない場合
	 */
	public boolean isRPCTargetMethod(Method method) {
		//staticメソッドの場合
		if ((method.getModifiers() & Modifier.STATIC) != 0) {
			//RPCの対象外にしたいので、falseを返す
			return false;
		}

		//可変引数を取るメソッドの場合
		if (method.isVarArgs()) {
			//RPCの対象外にしたいので、falseを返す
			return false;
		}

		//合成メソッド、またはブリッジメソッドの場合
		if (method.isSynthetic() || method.isBridge()) {
			//RPCの対象外にしたいので、falseを返す
			return false;
		}

		//Objectクラスで定義されているメソッドの場合
		//(toString()やequals()など)
		if (method.getDeclaringClass().equals(Object.class)) {
			//RPCの対象外にしたいので、falseを返す
			return false;
		}

		//メソッド名に「$」という文字が含まれている場合
		//(「$」はAOPで動的に作成されたメソッドにつけられる文字)
		//(AOPで動的に作成されたメソッドが呼び出されても困るので、対象から除外する)
		if (method.getName().indexOf('$') != -1) {
			//RPCの対象外にしたいので、falseを返す
			return false;
		}

		//最後までチェックをくぐり抜けたので、trueを返す
		return true;
	}

	/**
	 * 指定されたクラスオブジェクトの情報を元にして、RPCの対象となるメソッド情報のMapを取得します。<br>
	 * (本来は内部処理用のメソッドですが、外部からも呼び出させるようにpublicで定義しています。)<br>
	 *
	 * @param clazz クラスオブジェクト
	 * @return RPCの対象となるメソッド情報のMap
	 * @throws IllegalServiceClassException サービスクラスが不正な場合
	 */
	public Map<MethodNameAndArgsLength, Method> getRPCTargetMethodMap(
			Class clazz) throws IllegalServiceClassException {
		//クラスオブジェクトからクラス名を取得
		String className = clazz.getName();

		//フィールドのメソッド情報のキャッシュ用のMapから、クラス名をキーにしてRPCの対象となるメソッド情報のMapを取得する
		Map<MethodNameAndArgsLength, Method> map = this.methodCache.get(className);

		//キャッシュから取得できなかった場合
		if (map == null) {
			//結果を格納するMapを生成する
			map = new HashMap<MethodNameAndArgsLength, Method>();

			//クラスオブジェクトから取得した全てのメソッドに対してループをまわす
			for (Method method : clazz.getMethods()) {
				//RPCの対象となるメソッドの場合
				if (isRPCTargetMethod(method)) {
					//メソッド名と引数の数を保持するクラスのインスタンスを生成する
					MethodNameAndArgsLength methodNameAndArgsLength = new MethodNameAndArgsLength(method.getName(), method.getParameterTypes().length);

					//キー(メソッド名と引数の数)が一致する要素がすでにMapに登録されている場合
					if (map.containsKey(methodNameAndArgsLength)) {
						//サービスクラスが不正な場合の例外を投げる
						//(同じメソッド名で、引数の数も同じ場合はどれを呼び出して良いか識別できない場合があるのでエラー扱いにする)
						throw new IllegalServiceClassException(clazz.getName()
								+ "クラスの" + methodNameAndArgsLength.methodName
								+ "()メソッドで、引数の数が"
								+ methodNameAndArgsLength.argsLength
								+ "のものが複数定義されています。");
					}

					//メソッド名と引数の数を保持するクラスのインスタンスをキーにして、メソッドをMapに登録する
					map.put(methodNameAndArgsLength, method);
				}
			}

			//Mapが空の場合
			//(RPCの対象となるメソッドが1つも存在しない場合)
			if (map.isEmpty()) {
				//サービスクラスが不正な場合の例外を投げる
				throw new IllegalServiceClassException(clazz.getName()
						+ "クラスにはRPCの対象となるメソッドが1つも存在しません。");
			}

			//作成したMapをフィールドのメソッド情報のキャッシュ用のMapに登録する
			//(synchronizedで同期をとっていないので、複数スレッドで同時にこの処理を行った場合、
			// キャッシュのMapに登録されるオブジェクトがスレッド毎に異なる可能性があるが、
			// RPCの対象となるメソッド情報のMapは毎回同じ内容になるので問題ない)
			this.methodCache.put(className, map);
		}

		//キャッシュから取得、または作成したMapを返す
		return map;
	}

}
